﻿using System;
using System.Linq;
using System.Reflection;
using Affinity.Core.Exceptions;
using Affinity.Core.Utility;
using Affinity.Core.Validation;

namespace Affinity.Flux
{
    /// <summary>
    /// Represents a property into which data flows, or a method into which execution flows.
    /// </summary>
    public class Inlet
    {
        /// <summary>
        /// Factory method for creating an Inlet for the specified definition and component.
        /// </summary>
        /// <param name="inletDefinition"></param>
        /// <param name="component"></param>
        /// <returns></returns>
        public static Inlet Create(InletDefinition inletDefinition, object component)
        {
            Assert.ArgumentNotNull(inletDefinition, "inletDefinition");
            return new Inlet(component, inletDefinition.Member, inletDefinition.Contract, inletDefinition.Flags);
        }

        /// <summary>
        /// Factory method for creating an Inlet for the specified component, member (PropertyInfo or MethodInfo) and flags.
        /// </summary>
        /// <param name="component"></param>
        /// <param name="member"></param>
        /// <param name="contract"></param>
        /// <param name="flags"></param>
        /// <returns></returns>
        public static Inlet Create(object component, MemberInfo member, Contract contract, InletFlags flags)
        {
            return new Inlet(component, member, contract, flags);
        }

        /// <summary>
        /// Constructor accepting a reference to the owning component, the member (PropertyInfo or MethodInfo), and flags.
        /// </summary>
        /// <param name="component"></param>
        /// <param name="member"></param>
        /// <param name="contract"></param>
        /// <param name="flags"></param>
        protected Inlet(object component, MemberInfo member, Contract contract, InletFlags flags)
        {
            _Component = component;
            _Member = member;
            _Contract = contract;
            _Flags = flags;

            _Method = _Member as MethodInfo;
            _Property = _Member as PropertyInfo;
        }

        /// <summary>
        /// Gets the owning component.
        /// </summary>
        public object Component
        {
            get { return _Component; }
        }

        /// <summary>
        /// Gets the contract.
        /// </summary>
        public Contract Contract
        {
            get { return _Contract; }
        }

        /// <summary>
        /// Gets the flags.
        /// </summary>
        public InletFlags Flags
        {
            get { return _Flags; }
        }

        /// <summary>
        /// Gets the member (PropertyInfo or MethodInfo).
        /// </summary>
        public MemberInfo Member
        {
            get { return _Member; }
        }

        /// <summary>
        /// For a method inlet, gets a delegate corresponding to the method.
        /// If the inlet is a property inlet, an exception is thrown. 
        /// </summary>
        /// <param name="delegateType"></param>
        /// <returns></returns>
        public Delegate GetMethodDelegate(Type delegateType)
        {
            if (_Method == null) throw new Exception("Member is not a method.");

            try
            {
                return _Flags.HasFlag(InletFlags.ReceiveOnUIThread) ?
                    //Delegate.CreateDelegate(delegateType, this, GetInvokeMethod(_Method.GetParameters().Select(parameter => parameter.ParameterType).ToArray())) :
                    Delegates.CreateTypedDelegateForGeneralHandler(delegateType, InvokeMethod) :
                    Delegate.CreateDelegate(delegateType, Component, _Method);
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Exception creating delegate of type {0} for {1}: {2}", delegateType, this, ex));
            }
        }

        /// <summary>
        /// For a method inlet, invokes the method with the specified parameters.
        /// </summary>
        /// <param name="parameters"></param>
        public void InvokeMethod(object[] parameters)
        {
            if (Flags.HasFlag(InletFlags.ReceiveOnUIThread)) Threading.ExecuteOnUIThread(() => InvokeMethodCore(parameters));
            else InvokeMethodCore(parameters);
        }

        /// <summary>
        /// For a property inlet, set the property value to the specified value.
        /// </summary>
        /// <param name="value"></param>
        public void SetPropertyValue(object value)
        {
            if (_Property == null) throw new Exception("Member is not a property.");

            if (Flags.HasFlag(InletFlags.ReceiveOnUIThread)) Threading.ExecuteOnUIThread(() => SetPropertyValueCore(value));
            else SetPropertyValueCore(value);
        }

        /// <summary>
        /// Describes this object in a short manner.
        /// </summary>
        /// <returns></returns>
        public string ToShortString()
        {
            return string.Format("{0}.{1}", _Component.GetType().Name, _Member.Name);
        }

        /// <summary>
        /// Describes this object.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return string.Format("{0} (Contract: {1}, Hash: {2})", FormatMember(_Component, _Member), _Contract, GetHashCode());
        }

        /// <summary>
        /// Returns a string representing the specified member.
        /// </summary>
        /// <param name="component"></param>
        /// <param name="member"></param>
        /// <returns></returns>
        private static string FormatMember(object component, MemberInfo member)
        {
            return string.Format("{0}.{1}", member.DeclaringType.Name, member.Name);
        }

        /// <summary>
        /// For a method inlet, invokes the method with the specified parameters.
        /// </summary>
        /// <param name="parameters"></param>
        private void InvokeMethodCore(object[] parameters)
        {
            try
            {
                _Method.Invoke(_Component, parameters);
            }
            catch (Exception ex)
            {
                throw new ExceptionWrapper(new ExceptionWrapper.Parameters 
                                            {
                                                Message = string.Format("{0}{1}(Caught while invoking method {2} on object of type {3} for contract {4}.)", ex.GetBaseException().Message, Environment.NewLine, _Member.Name, _Component == null ? null : _Component.GetType().Name, _Contract),
                                                WrappedException = ex, 
                                                StackTrace = ex.StackTrace 
                                            });
            }
        }

        /// <summary>
        /// For a property inlet, sets the property value to the specified value.
        /// </summary>
        /// <param name="value"></param>
        private void SetPropertyValueCore(object value)
        {
            try
            {
                _Property.SetValue(_Component, value, null);
            }
            catch (Exception ex)
            {
                throw new ExceptionWrapper(new ExceptionWrapper.Parameters
                {
                    Message = string.Format("{0}{1}(Caught while setting property {2} on object of type {3} for contract {4}.)", ex.GetBaseException().Message, Environment.NewLine, _Member.Name, _Component == null ? null : _Component.GetType().Name, _Contract),
                    WrappedException = ex,
                    StackTrace = ex.StackTrace
                });
            }
        }

        private readonly object _Component;
        private readonly Contract _Contract;
        private readonly InletFlags _Flags = InletFlags.None;
        private readonly MemberInfo _Member;
        private readonly MethodInfo _Method;
        private readonly PropertyInfo _Property;
    }
}
